tags:
- OS
早期由于8086/8088 CPU最大只能寻址1MB且指令长度为16位。为了兼容性,x86系列的CPU在计算机启动后的最开始都以实模式(Real Mode)运行。实模式下只能使用低1MB的RAM,而且默认的指令长度为16bits。
在实模式下,地址线直接映射物理内存地址。处理器不执行任何形式的内存管理和保护,如BIOS阶段的指令,没有我们后面介绍的特权级别一说,也没有特权指令、非特权指令之分。在实模式下,处理器会执行BIOS阶段的指令,初始化硬件和系统设置。
之后,Bootloader接管计算机并在适当时刻将处理器从实模式转变为 保护模式(Protected Mode) 这一转变使得系统可以访问更大的内存空间和执行更复杂的指令,也可以使用例如虚拟内存管理和内存保护机制这样的高级功能。所有的现代操作系统都运行在保护模式。
为了实现系统的安全性和稳定性,现代OS和处理器定义了两种处理器执行权限模式:用户模式和内核模式。 这种设计使得操作系统可以更好地管理硬件资源,防止用户程序对系统的核心部分进行未经授权的访问。
在用户模式下,程序只能执行有限的指令,不能直接对硬件和内核中的数据结构进行访问,这种指令被称为非特权指令(non-privileged instructions)。用户模式的限制防止用户程序对系统资源的滥用,保护了系统的安全和稳定。常见的非特权指令有:
也叫Kernel mode。在内核模式下,操作系统内核对所有的硬件和软件资源具有完全的访问权限。内核可以执行任何指令,管理任何设备和系统资源。只能在内核模式下运行的指令就是特权指令(privileged instructions),这些指令通常涉及对硬件资源的直接控制和管理。常见的特权指令有:
在保护模式下,内核和用户程序从此隔离。用户程序只能够执行一些特定的指令,称为非特权指令,而内核可以执行任何特权、非特权的指令。那系统(CPU)是如何知道谁是用户程序,谁是内核程序呢?
现代操作系统实现了4种不同的程序执行等级(CPU模式),称为 hierarchical protection domains,也叫 protection rings。实际上这四种不同的 rings 只使用了两种。其中,用户程序只能执行在 Ring 3,而内核程序执行在 Ring 0。CPU 通过 CS 寄存器的前两位来识别当前程序的特权级别,称为 CPL (Current Privilege Level) 位。
在段描述符、门描述符中还有 DPL(Descriptor Privilege Level)用来表示描述符特权级。比如在中断门描述符每一项都有一个 DPL,用于控制中断请求的特权级别;系统调用所对应的门描述符也有一个 DPL,用于控制系统调用的特权级别。只有当 CPL ≤ DPL 时,才能访问该描述符。
后面我们在学习进程6. Processing The Processes阶段的时候,我们会了解到每个进程都会有内核区(在32位系统下虚拟内存为3GB-4GB),在内核区中的代码段描述符和数据段描述符等的DPL就会被设置为0,即只有CPL为0时才能访问这些代码和数据等。
我们对用户模式和内核模式进行了简单的了解,我们知道用户程序都运行在用户模式下(Ring 3),权限受限。而操作系统核心组件(如内存管理、进程调度、文件系统等)运行在内核模式下(Ring 0),在用户态的用户程序是无法直接对系统的核心组件进行篡改的。
为了确保系统的安全,我们需要保证系统的完整性。系统完整性即系统在运行过程中保持其预期的正确性和一致性,防止未经授权的修改和破坏。由于用户态和内核态的分离,对系统核心的操作都在内核态中进行,这种特性保护了系统的完整性和稳定性。
比方来说,在用户程序下,如果出现程序错误,往往只会导致程序崩溃,而不会影响整个系统的稳定性。通过内核模式的错误处理机制,操作系统可以捕获和处理这些错误,防止它们扩散到系统的其他部分,维护系统的整体完整性。而且用户程序是无法直接篡改系统的核心部件的,这也维护了系统的完整性。
策略(policy) 是指操作系统如何决定何时以及如何执行某些操作,例如进程调度、内存管理、资源分配等。策略规定了系统行为的高层规则,而机制(mechanism) 则是实现这些规则的具体方法。
操作系统在内核模式下执行具体的机制(如进程调度、内存管理),而策略(如调度策略、资源分配策略)则由操作系统的高层组件或管理员来决定。这种分离有助于系统的灵活性和可维护性,同时确保系统的完整性。
由于操作系统对底层硬件的保护和封装,用户程序是不能够直接操作计算机硬件的。如果用户程序想要使用系统资源怎么办呢?操作系统通过系统调用接口提供了一系列的机制,使得用户程序可以调用这些接口来请求操作系统执行特定的操作,操作系统完成后返回操作结果。
系统调用是操作系统提供给应用程序的一组接口,允许应用程序请求操作系统执行一些不能在用户模式下完成的操作。系统调用是应用程序与操作系统内核之间的桥梁。对于用户而言,使用系统调用和调用函数非常类似,唯一的不同就是系统调用工作在内核模式。
系统调用的作用就是提供了一种安全的方法,让用户空间的程序请求内核空间的服务。我们通常将执行文件操作、进程控制、网络通信等需要更高权限的操作进行封装,提供系统调用接口给用户程序去使用。
当应用程序需要执行一个系统调用时,它会通过特定的机制(如软中断)通知操作系统。操作系统接收到通知后,会从用户模式切换到内核模式,执行相应的内核函数。完成操作后,操作系统将结果返回给应用程序,并切换回用户模式。这就是系统调用的过程。
系统调用有什么好处呢?它可以让操作系统在满足请求之前检查请求的正确性,防止不安全的操作。同时,应用程序开发者不需要了解硬件的低级编程细节。此外,相同的系统调用通常上在相同的操作系统上的接口是一样的,使程序具有一定的可移植性。
open()
System Call我们用打开文件open()
函数举例,假设我们有以下 C 程序:
#include <stdio.h>
#include <fctrl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main(){
/*用户态*/
int fd = open("example.txt", O_RDWR, S_IRUSR | S_IWUSR);//Mode switching
if(fd == -1){
perror("fail to open file");
return 1;
}
char buf[128];
int size = read(fd, buf, sizeof(buf) - 1);//Mode switching
if(size == -1){
perror("fail to read");
close(fd);//Mode switching
return 1;
}
printf("Read form buf: %s", buf);//Mode switching
close(fd);//Mode switching
return 0;
}
虽然这段代码不长,但是发生了4次系统调用,状态转换了8次。
strace
Question: Why printf();
works in the user mode?
Answer: printf();
函数运行在用户态是因为 C standard library(libc) 运行在用户态,而 printf();
是作为标准库的一部分。好了,现在的问题转为为什么标准库运行在用户态了,在我们调用 printf();
的时候首先 printf();
函数会根据函数里面的参数将打印的字符串序列排好序。之后字符串会被放到一个buffer中,然后调用系统调用write()
将buffer传给内核处理。内核将buffer里面的内容交给特定的输出设备然后回到用户态,打印结束。
printf
in Terminal假设我们有如下的代码,我们对其进行编译并使用strace
命令进行系统调用跟踪,会发生什么?
#include<stdio.h>
int main(){
printf("hello");
return 0;
}
我们发现当我们执行程序的时候,我们调用了非常多的系统调用,但是在最后面有这么一行:write(1, "hello", 5hello) = 5
。表示printf
函数实际上是使用write
系统调用来向屏幕上打印 "hello" 字符串的,返回值为5,即打印了5个字符。
du@DVM:~/Desktop/DSA$ strace ./test
execve("./test", ["./test"], 0x7ffdd5170160 /* 55 vars */) = 0
brk(NULL) = 0x570c55c3b000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc25af5710) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7cdffcdbc000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=59619, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 59619, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7cdffcdad000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0I\17\357\204\3$\f\221\2039x\324\224\323\236S"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2220400, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2264656, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7cdffca00000
mprotect(0x7cdffca28000, 2023424, PROT_NONE) = 0
mmap(0x7cdffca28000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7cdffca28000
mmap(0x7cdffcbbd000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7cdffcbbd000
mmap(0x7cdffcc16000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x215000) = 0x7cdffcc16000
mmap(0x7cdffcc1c000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7cdffcc1c000
close(3) = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7cdffcdaa000
arch_prctl(ARCH_SET_FS, 0x7cdffcdaa740) = 0
set_tid_address(0x7cdffcdaaa10) = 71252
set_robust_list(0x7cdffcdaaa20, 24) = 0
rseq(0x7cdffcdab0e0, 0x20, 0, 0x53053053) = 0
mprotect(0x7cdffcc16000, 16384, PROT_READ) = 0
mprotect(0x570c55b39000, 4096, PROT_READ) = 0
mprotect(0x7cdffcdf6000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7cdffcdad000, 59619) = 0
newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}, AT_EMPTY_PATH) = 0
getrandom("\x81\x42\x61\xea\x7a\xbb\x01\x45", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x570c55c3b000
brk(0x570c55c5c000) = 0x570c55c5c000
write(1, "hello", 5hello) = 5
exit_group(0) = ?
+++ exited with 0 +++
我们对上面的系统调用重新看一遍,为什么只是打印一个字符串确需要如此多的系统调用?这是因为操作系统需要处理许多底层任务来支持程序的运行,我们执行printf
之前的加载并执行程序都需要操作系统的介入。
系统调用是一种特殊类型的软件中断,通过系统调用,用户空间的程序可以请求操作系统的服务。系统调用是现代操作系统的基础功能,它为用户程序可以安全地使用内核模式下才能执行的文件操作、进程控制、通讯等操作。
系统调用有多种的实现方式:
int 0x80
的软件中断方式,其中 0x80
就是系统调用的中断向量号。syscall
指令来使用系统调用。 syscall
并不使用传统的中断向量表,而直接通过特定的 MSR(模型特定寄存器) 跳转到内核系统调用实现的入口点。当使用传统x86系统上的系统调用时,每个系统调用都会有唯一一个标识符,也就是系统调用号。系统调用号用于区分不同的系统调用。用户程序通常将系统调用号放在一个特定的寄存器(比如 eax
寄存器)中来指示希望执行哪个系统调用。随后内核通过查看这个寄存器来决定执行具体的ISR。
当使用现代64位的x86系统时,保存系统调用号和系统调用入口关系的数据结构就是系统调用表(syscall table)。
向openat
这样的函数实际上是系统调用号的包装器。这些包装函数简化了系统调用的使用,允许程序员通过高级和更易使用的接口方式与操作系统交互。
我们用 open("filename", MODE)
库函数为例,我们在用户代码中被调用时只传入两个参数:文件名和打开文件的模式。在该函数执行时会触发模式切换并调用openat系统调用,不同的CPU架构实现方法不太一样,下面我们对比3种架构。
我们会看到,虽然实现的细节不同,但是它们系统调用指令和传递参数的方式都是类似的。
x86-32属于传统的x86系统,使用 int 0x80
来调用 Linux 内核。如下:
section .data
filename db 'example.txt', 0 ; 要打开的文件名,末尾有一个null字符
filemode dw 0x0002 ; O_RDONLY模式
section .text
mov eax, 5 ; open系统调用的编号是5
mov ebx, filename ; 第一个参数
mov ecx, filemode ; 第二个参数
xor edx, edx ; 第三个参数
int 0x80 ; 执行系统调用
到了x86-64位系统上,用系统调用表(syscall table) 来保存记录系统调用号和系统调用入口的关系。我们使用 syscall
来执行系统调用。我们忽略一些细节,相关调用实现如下:
section .data
filename db 'example.txt', 0 ; 要打开的文件名,末尾有一个null字符
section .text
mov rax, 257 ; openat的系统调用的编号
mov rdi, -100 ; AT_FDCWD,当前 工作目录
mov rsi, filename ; 第一个参数
mov rdx, O_RDONLY ; 第二个参数
xor r10, r10 ; 第三个参数
syscall ; 执行系统调用
.section .data
filename:
.ascii "example.txt\0"
.text
mov x0, -100
ldr x1, =filename
mov x2, 0
mov x3, 0
mov x8, 56
svc 0
Interrupts are interruption to CPU.
系统调用是一种特殊的中断。所有中断的处理流程和系统调用的处理流程有很多相似的地方。现代计算机是中断驱动的,中断机制的存在使得计算机(OS)能够及时响应外部事件并作出反应,极大地提升了系统的效率和响应性。
如果没有中断机制,CPU将不得不采用轮询(Polling)方式逐个地检查每个设备的状态以确定其是否需要服务。这是一种主动响应机制,相比于被动响应的中断机制,轮询浪费了很大一部分计算资源。
中断(Interrupts) 是一个由硬件或软件发出的信号,当某个过程或事件需要立即处理时会使用中断来通知处理器。中断的目的是让处理器注意到更高优先级的任务并打断当前正在进行的指令流保存当前的状态,然后转而去执行特定的中断处理程序,最后再返回原指令流中继续执行,这就是 中断机制 。
根据中断信号的产生源可将中断分为硬件中断和软件中断两大类:
Hardware Interrupt:
外部中断(External Interrupt):来自CPU外部硬件或I/O设备的中断。
内部中断(Internal Interrupt):CPU内部自已产生的中断,也称为异常(exception)。
Software Interrupt:
Hardware Interrupts :
Traps :
Exceptions :
操作系统是如何区分处理各种中断信号的。每种中断信号都被赋予一个特定的编号,称为中断向量号。这个向量号是一个整数,它起到了关键的角色,让系统能够识别并对应到具体的中断处理程序(也叫“中断服务例程”)。
中断向量号的分配方式取决于中断的类型:
在32位的x86架构中,有256个中断号,从0到255,也就是int
指令后面的数字最大是255。中断向量号的划分如下:
我们在前面看到过,在下x86架构下的系统调用编号是0x80
。我们可以说,系统调用是软件中断的特定形式,而软件中断又是中断的子集。
中断向量表(Interrupt Vector Table,IVT) 存储了每个中断向量号与其对应的 中断服务例程(Interrupt Service Routine, ISR) 的地址。当系统检测到一个中断信号时,它会通过这个信号的向量号在IVT中查找相应的服务例程地址,然后跳转到该地址执行中断处理程序。
在计算机的实模式中,像键盘输入和屏幕显示等BIOS服务都是通过预设的中断方式实现的,它使用了中断向量表来查询BIOS中断号对应的ISR地址。
当计算机转入保护模式, 中断描述符表(Interrupt Descriptor Table, IDT) 取代了IVT,成为新的中断管理核心。IDT 相较于 IVT,有以下优点(不完整):
中断服务例程(Interrupt Service Routine, ISR) 是响应硬件或软件中断信号的一段特定程序代码。当某个事件(如输入/输出操作、时钟信号、硬件故障或其他外部事件)触发中断时,处理器会暂停当前正在执行的任务,转而执行与该中断关联的ISR。
当系统在执行指令流的过程中,在完成第 i 条指令后,如果突然接收到一个中断信号或主动发起系统调用,此时系统需要暂停当前的任务去响应这个中断/调用请求。为了确保中断处理完毕后,程序能够无缝地返回到被中断的位置继续执行后续指令,系统必须在执行中断处理程序之前,保存必要的一些信息,即现场信息。
以x86-32位架构为例。(arch/x86/kernel/entry_32.S
)
现场信息是指CPU中的一些寄存器的值,通过保存恢复这些寄存器的数值,程序就能够回到被中断位置继续执行指令。这其中有几个核心寄存器:
那这些现场信息存放在哪里呢?只要程序暂停执行并在未来需要恢复,那么程序就需要把现场信息先存放到一个位置,这个位置就是内核栈。这些现场信息会被 push
到内核栈中。
一旦程序在用户态被中断,上面提到的五个最重要的寄存器由硬件 CPU 自动完成(这个操作是当即完成的),无需操作系统或中断处理程序进行任何干预。而进程的中断上下文不仅仅只有这五个寄存器。操作系统负责保存一些额外的现场信息内容(执行 ISR 时保存)。如:
EAX
, EBX
, ECX
, EDX
, ESI
, EDI
, EBP
)DS
, ES
, ES
, GS
)如果有错误发生,硬件还会压入错误码。待操作系统处理。
此外,在 CPU 执行 ISR 时,还牵扯到 CPL 的变化。即在中断发生时 CPU 特权级别 CPL 是"user mode", CPU 需要负责将 CPL 切换至 0,即"kernel mode",才能接着执行 ISR。(中断门描述符检查 DPL ≥ CPL )
如果程序在内核态中断,那么将不再需要保存 SS
寄存器和 ESP
寄存器。因为中断时指向的就是内核栈。另外的,内核态下的段寄存器(如DS、ES)一般也不需要保存。因为它们已经指向内核数据段。
每个线程都有自己的内核栈。当线程被中断时,CPU 就会转换到其内核栈将线程的上下文进行压栈保存。一般而言,内核栈大小为 8KB/16KB 。
以x86-32架构为例。
在 iret
指令执行前,中断服务例程(ISR)需手动恢复操作系统保存的寄存器。恢复顺序需与保存顺序严格相反(后进先出,LIFO)。
ISR的指令逻辑流程:
中断服务例程的执行完成后,系统需要通过执行 iret
指令来从 ISR 返回到原来的程序执行流。iret
指令的作用不仅是精确和有序的,它还负责恢复之前由 CPU 自动保存到栈中的处理器状态,并实现特权级的适当切换。
如果返回到用户态(CPL 0->3),那么执行 iret
时寄存器会依次弹出 EIP、CS、EFLAGS、用户态 ESP、用户态 SS。然后恢复 CS 中 CPL 的特权级别为 3,CPU 自动换回用户态。
仅仅弹出 EIP、CS、EFLAGS。而且由于特权级别不变,不切换栈指针。
信号将在IPC阶段中介绍。
学习Linux的信号机制(signaling mechanism)时,一时间将我的思绪拉回了中断。信号是什么?软中断是什么?软件中断又是什么?它们有何相同点?又有哪些是不同的?
The signaling mechanism in the Linux kernel allows running applications to asynchronously notify the system when a new event occurs. Because of its nature, this signaling mechanism is generally known as software interrupts.
信号在Linux操作系统中非常重要。信号是进程间通信的一种方式。对于内核而言,信号就是一个事件,操作系统内核将中断的处理结果以信号的方式传递给进程。随后进程根据处理的结果做出相应的动作。
我们常常用信号作为进程间通信IPC和异常处理的一种手段。虽然操作系统内核通常是大多数信号的来源,但下面我们仍列举一些其他的信号来源:
kill
系统调用产生的信号我们下面简单介绍硬件中断和异常是如何产生信号的。之后小节我们用例子说明kill
系统调用是怎么产生信号并终止特定进程的。
信号的本质就是软件层次对中断的模拟,是一种异步通信机制。每个信号都对应着不同的功能。举个例子(按下Ctrl + C):
Ctrl+C
按键组合,并发送硬件中断信号给 CPU。假如我们有一个signal.c
文件如下:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void divideByZeroHandler(int signum) {
printf("Divide by zero exception occurred!\n");
exit(1);
}
int main() {
if (signal(SIGFPE, divideByZeroHandler) == SIG_ERR){
perror("Cannot register the handler");
return 1;
}
int dividend = 10;
int divisor = 0;
int result;
printf("Attempting to divide %d by %d...\n", dividend, divisor);
result = dividend / divisor;
printf("Result: %d\n", result);
return 0;
}
我们知道,当除数为0时,结果会是一个异常。当这个异常发生时,进程就会中断。这时操作系统内核会把 SIGFPE(浮点异常) 发给进程。又由于我们设置了信号的服务程序signal(SIGFPE, divideByZeroHandler);
,所以当进程收到信号,他就会打印输出信息并退出程序 error(1)。
信号由操作系统负责传递给指定进程,传递时机是:
进程收到信号后的处理方式有:
我们可以通过下面的命令来查看Linux中的所有类型的信号:
kill -l
Linux系统中有62种信号,分为两类:非实时信号(1-31)和实时信号(34-64)。非实时信号包括常见的SIGINT(中断信号)、SIGTERM(终止信号)等,而实时信号用于更高精度的事件处理。
从上述信号的了解中,我们能够感受到,中断可以是信号的产生源。我们已经学过中断了,我们可以将中断定义为一种CPU和操作系统内核的交流方式。中断一旦发生,CPU 暂停当前任务并触发内核中的中断服务程序(ISR)。
处理程序(Handler):信号处理程序在用户空间代码中执行,而中断服务程序在内核空间中执行。信号处理程序用于处理进程接收到的特定信号,而中断服务程序用于处理硬件中断。
屏蔽(Mask):信号屏蔽和中断屏蔽分别用于暂时阻止进程接收信号和处理硬件中断,以保护关键代码段的原子性执行
系统调用(syscall)是一种软件中断,我们可以用系统调用来给特定的进程发送信号。如:
kill
系统调用:用于向指定进程发送信号,可以通过进程ID(kill(PID)
)来指定目标进程。raise
系统调用:用于向自身进程发送信号,相当于调用 kill(getpid(), sig)
。alarm
系统调用:设置一个定时器,当定时器到期时,会向进程发送 SIGALRM
信号。sigqueue
系统调用:用于向指定进程发送信号,并可以附带一个值。kill
系统调用提供kill
系统调用,我们可以给特定的进程发送SIGINT
信号来终止某个进程。如下:
#include <sys/types.h>
#include <signal.h> // An abstraction to raw syscall
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
for (int i = 0; i < 5; ++i) {
printf("this is child process\n");
sleep(1);
}
} else if (pid > 0) {
printf("this is parent process\n");
sleep(2);
printf("terminating child process now!\n");
kill(pid, SIGINT);
} else {
perror("fork");
}
return 0;
}
在这个例子中,父进程先是使用 fork()
创建一个子进程。子进程每秒打印一次 “this is child process”。父进程等待2秒后,使用 kill(pid, SIGINT)
向子进程发送 SIGINT
信号,终止子进程。我们还可以自己编写信号处理函数来处理输出我们想要的结果。
du@DVM:~/Desktop/CppCode$ ./kill
this is child process
this is parent process
this is child process
this is child process
terminating child process now!
中断的来源很多,softirq的种类也不少。内核的限制是不能超过32个,目前实际用到的有10个。包括高优先级tasklet、定时器、网络收发、块设备、普通tasklet、高精度定时器和RCU等。softIRQ主要用于处理高频率、低延迟的任务,如网络包处理和定时器等。
其中两个用来实现tasklet(HI_SOFTIRQ和TASKLET_SOFTIRQ),两个用于网络的发送和接收操作(NET_TX_SOFTIRQ和NET_RX_SOFTIRQ),一个用于调度器(SCHED_SOFTIRQ),实现SMP系统上周期性的负载均衡。在启用高分辨率定时器时,还需要一个HRTIMER_SOFTIRQ。
为了有效地管理不同的softirq中断源,Linux采用的是一个名为softirq_vec[] 的数组,数组的大小由NR_SOFTIRQS 表示,这是在编译时就确定了的,不能在系统运行过程中动态添加。每个数组元素代表一种softirq的种类,而数组里存放的内容则是其各自对应的执行函数。